/* 全局变量 */
// WebSocket聊天/信令通道变量。
var connection = null;
var clientID = 0;
var mediaConstraints = {
  audio: true,
  video: true
};

// 记录本地流 以便关闭
var localStream = null;

isToPeer = false;               // 是否建立了点对点连接
var isCall = false;
var myUsername = null;
var targetUsername = null;      // 存储其他对等点的用户名
var myPeerConnection = null;    // RTC对等连接
// 不管用不用addTrack()，我们需要注意它是否可用
var hasAddTrack = false;
var localVideo = document.getElementById("local_video");
var receivedVideo = document.getElementById("received_video");
var headerHint = document.getElementById("hint");

var videToPeer = document.getElementById("videToPeer");
videToPeer.style.display = 'none';

/*

  用户分为 A、B 两方
  呼叫方为 A, 接收方为 B
*/

/*
  A 方方法
*/
// 发起呼叫
function apply(evt) {
  if (isCall) {
    alert("你不能打电话，因为你已经有一个空电话了!");
  } else {
    var clickedUsername = evt.target.textContent;
    // 不允许用户称呼自己，因为很奇怪。
    if (clickedUsername === myUsername) {
      alert("恐怕我不能让你自言自语。那太奇怪了。");
      return;
    }
    // 记录被调用的用户名，以备将来参考
    // targetUsername = clickedUsername;

    headerHint.innerText = '正在等待对方回复中。。。';
    // 发送请求并等待对方同意
    sendToServer({
      name: myUsername,
      target: clickedUsername,
      id: clientID,
      type: "apply-video"
    });
  }
}

/*
  监听到 ‘new-ice-candidate’ 消息
  一位新的ICE候选人已经从另一位同事那里收到。
  调用RTCPeerConnection.addIceCandidate()将其发送到本地ICE框架。
*/
function handleNewICECandidateMsg(msg) {
  var candidate = new RTCIceCandidate(msg.candidate);
  myPeerConnection.addIceCandidate(candidate)
    .catch((err)=>{
      console.log('handleNewICECandidateMsg 报错：'+err);
    });
}
// 收到回复
async function handleReplyVideoMsg(msg){
  switch(msg.replyType) {
    case 1:
      // 对方同意
      isCall = msg.name;
      // 对方同意之后创建自己的 peer
      try {
        await createP2P(msg.name);
        // 并给对方发送 offer
        startCreateOffer(msg);
      } catch (error) {
        console.log('获取摄像头失败：'+error);
      }
      break;
    case 2:
      alert('对方拒绝了你的视频通话请求！');
      headerHint.innerText = '输入昵称进入房间后点击用户名邀请视频通话';
      break;
    case 3:
      alert('对方正在通话中！');
      headerHint.innerText = '输入昵称进入房间后点击用户名邀请视频通话';
      break;
  }
}
// 创建并发送 offer
async function startCreateOffer(msg){
  try {
    // 创建offer
    let offer = await myPeerConnection.createOffer();
    // 呼叫端设置本地 offer 描述
    await myPeerConnection.setLocalDescription(offer);
    targetUsername = msg.name;
    // 给对方发送 offer
    sendToServer({
      name: myUsername,
      target: targetUsername,
      type: "video-offer",
      sdp: myPeerConnection.localDescription // offer
    });
  } catch (e) {
    console.log('startCreateOffer: ', e);
  }
}
// 接收answer
// 一旦被来电者决定接受我们的通话请求，立即回复发送给来电者的“视频应答”信息。
function handleVideoAnswerMsg(msg) {
  log("电话接收者已接受我们的电话");
  var desc = new RTCSessionDescription(msg.sdp);
  myPeerConnection.setRemoteDescription(desc).catch(reportError);
}


/* B方法 */
// 收到邀请
async function handleApplyVideoMsg(msg) {
  var initiatorName = msg.name;
  // 如果正在通话中
  if(isCall) {
    // 通知对方回复消息
    reply(initiatorName, 3);
    return;
  }
  // 在提示是否允许
  if (confirm(initiatorName+"向你请求视频通话, 是否同意?")){
    try {
      await createP2P(initiatorName);
      isCall = initiatorName;
      reply(initiatorName,1);
    } catch (error) {
      console.log('获取摄像头失败：'+error);
      reply(initiatorName,2);
    }
  }else{
    reply(initiatorName,2);
  }
}
/*
  请求处理回复
  replyType 取值
    1 表示同意
    2 表示拒绝
    3 表示正则通话中
*/
function reply(targetUsername,replyType) {
  sendToServer({
    name: myUsername,
    target: targetUsername,
    id: clientID,
    replyType: replyType,
    type: "reply-video"
  });
}
/*
  接收到 offer
  接受视频聊天的邀请。我们配置我们的本地设置，
  创建我们的RTCPeerConnection，获取并附加我们的本地摄像机流，
  然后创建并发送一个答案给调用者。
*/
async function handleVideoOfferMsg(msg) {
  try {
    var desc = new RTCSessionDescription(msg.sdp);
    // 接收端设置远程 offer 描述
    await myPeerConnection.setRemoteDescription(desc);
    // 接收端创建 answer
    let answer = await myPeerConnection.createAnswer();
    // 接收端设置本地 answer 描述
    await myPeerConnection.setLocalDescription(answer);
    // 赋值给 targetUsername
    targetUsername = msg.name;
    // 给对方发送 answer
    sendToServer({
      name: myUsername,
      target: targetUsername,
      type: "video-answer",
      sdp: myPeerConnection.localDescription // answer
    });
  } catch (e) {
    console.log('onOffer: ', e);
  }
}



/* A和B公用方法 */
async function createP2P(targetUsername){
  localStream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
  localVideo.srcObject = localStream;
  // 如果是 本地 localhost 部署，这个可以忽略
  myPeerConnection = new RTCPeerConnection({
    // 关于ICE服务器的信息-使用您自己的!
    "iceServers": [{
      urls: 'stun:stun.l.google.com:19302'
    }, {
        "urls": "turn:numb.viagenie.ca",
        "username": "webrtc@live.com",
        "credential": "muazkh"
    }],
    sdpSemantics:'plan-b'
  });
  myPeerConnection.addStream(localStream); // 添加本地流
  // 监听ICE候选信息 如果收集到，就发送给对方
  myPeerConnection.onicecandidate = function(event) {
    if (event.candidate) {
      sendToServer({
        name: myUsername,
        type: "new-ice-candidate",
        target: targetUsername,
        candidate: event.candidate
      });
    }
  }
  hasAddTrack = (myPeerConnection.addTrack !== undefined);
  // 监听是否有媒体流接入，如果有就赋值给 rtcB 的 src
  if (hasAddTrack) {
    myPeerConnection.ontrack = (event) => {
      isToPeer = true;
      headerHint.innerText = '与'+targetUsername+'通话中';
      videToPeer.style.display = 'block';
      receivedVideo.srcObject = event.streams[0];
      document.getElementById("hangup-button").disabled = false;
    }
  } else {
    myPeerConnection.onaddstream = (event) => {
      isToPeer = true;
      headerHint.innerText = '与'+targetUsername+'通话中';
      videToPeer.style.display = 'block';
      receivedVideo.srcObject = event.stream;
      document.getElementById("hangup-button").disabled = false;
    };
  }
}
/*
  挂断电话
  挂断电话的方法是关闭连接的一端，然后向另一端发送一条“挂断”消息(请记住，信号是在另一个连接上完成的)。这将通知另一个对等点连接应该终止，UI返回到“no call in progress”状态。
*/
function hangUpCall() {
  myPeerConnection.close();
  myPeerConnection = null;
  isToPeer = false;
  headerHint.innerText = '输入昵称进入房间后点击用户名邀请视频通话';
  // 关闭摄像头
  localStream.getTracks().forEach(function (track) {
    track.stop();
  });
  videToPeer.style.display = 'none';
  isCall = false;
  sendToServer({
    name: myUsername,
    target: targetUsername,
    type: "hang-up"
  });
}
// 监听挂断消息 "hang-up"
function handleHangUpMsg(){
  alert('对方已经挂断');
  myPeerConnection.close();
  myPeerConnection = null;
  isToPeer = false;
  headerHint.innerText = '输入昵称进入房间后点击用户名邀请视频通话';
  // 关闭摄像头
  localStream.getTracks().forEach(function (track) {
    track.stop();
  });
  videToPeer.style.display = 'none';
  isCall = false;
}

//==========================================webscoket=============================================================================================
/*

  websocket 内容

*/
// 打开并配置到WebSocket服务器的连接。
function connect() {
  connection = new WebSocket('wss://localhost:6503/', "json");
  // 连接建立时自动触发
  connection.onopen = function(evt) {
    document.getElementById("text").disabled = false;
    document.getElementById("send").disabled = false;
  };
  // 通信发生错误时触发
  connection.onerror = function(evt) {
    console.dir(evt);
  }
  // 连接关闭时触发
  connection.onclose = function(evt) {
    console.log('关闭'+evt);
    hangUpCall();
  }
  // 客户端接收服务端数据时触发
  connection.onmessage = function(evt) {
    var chatFrameDocument = document.getElementById("chatbox");
    var text = "";
    var msg = JSON.parse(evt.data);
    console.log(msg);
    log("Message received: ");
    console.dir(msg);
    var time = new Date(msg.date);
    var timeStr = time.toLocaleTimeString();
    switch(msg.type) {
      case "id":
        clientID = msg.id;
        setUsername();
        break;
      case "username":
        text = "<b>User <em>" + msg.name + "</em> signed in at " + timeStr + "</b><br>";
        break;
      case "message":
        text = "(" + timeStr + ") <b>" + msg.name + "</b>: " + msg.text + "<br>";
        break;
      case "rejectusername":
        myUsername = msg.name;
        text = "<b>Your username has been set to <em>" + myUsername +
          "</em> because the name you chose is in use.</b><br>";
        break;
      case "userlist":      // 收到更新的用户列表
        handleUserlistMsg(msg);
        break;
      // 信令消息:这些消息用于在视频通话之前的谈判期间交换WebRTC信令信息。
      case "video-offer":  // 发送 offer
        handleVideoOfferMsg(msg);
        break;
      case "apply-video":  // 收到聊天邀请
        handleApplyVideoMsg(msg);
        break;
      case "reply-video":  // 收到邀请回复
        handleReplyVideoMsg(msg);
        break;
      case "video-answer":  // Callee已经答复了我们的报价
        handleVideoAnswerMsg(msg);
        break;
      case "new-ice-candidate": // 一位新的ICE候选人已经收到
        handleNewICECandidateMsg(msg);
        break;
      case "hang-up": // 另一位同事挂断了电话
        handleHangUpMsg(msg);
        break;
      // 未知的信息;输出到控制台进行调试。
      default:
        log_error("未知的信息收到了:");
        log_error(msg);
    }

    // 如果有文本要插入聊天缓冲区，现在就插入，然后滚动聊天面板，这样就可以看到新文本。
    if (text.length) {
      // chatFrameDocument.write(text);
      document.getElementById("chatbox").innerHTML += `<p>${text}</p>`;
    }
  };
}

// 给定包含用户名列表的消息，此函数将用这些名称填充用户列表框，使每个项都可单击，从而允许启动视频调用。
function handleUserlistMsg(msg) {
  var i;
  var listElem = document.getElementById("userlistbox");
  // 删除所有当前列表成员。我们可以更聪明地做到这一点，通过添加和更新用户，而不是从头开始重新构建，但这将对这个示例有效。
  while (listElem.firstChild) {
    listElem.removeChild(listElem.firstChild);
  }
  // 如果通话的另一方离开了房间
  if(isToPeer&&!msg.users.includes(targetUsername)) {
    handleHangUpMsg();
  }
  // 从接收列表中添加成员名
  for (i=0; i < msg.users.length; i++) {
    var item = document.createElement("li");
    item.appendChild(document.createTextNode(msg.users[i]));
    item.addEventListener("click", apply, false);
    listElem.appendChild(item);
  }
}

// 将 JavaScript 对象转换为 JSON 并在WebSocket连接上作为消息发送。
function sendToServer(msg) {
  var msgJSON = JSON.stringify(msg);
  log("Sending '" + msg.type + "' message: " + msgJSON);
  connection.send(msgJSON);
}

/*
  在接收到“id”消息时调用;此消息由服务器发送，以分配此登录会话的唯一ID号;
  作为响应，这个函数发送一个“username”消息来设置会话的用户名。
*/
function setUsername() {
  myUsername = document.getElementById("name").value;
  sendToServer({
    name: myUsername,
    date: Date.now(),
    id: clientID,
    type: "username"
  });
}

// 通过构建一个“message”对象并将其发送到服务器来处理单击Send按钮(或按return/enter)。
function handleSendButton() {
  var msg = {
    text: document.getElementById("text").value,
    type: "message",
    id: clientID,
    date: Date.now()
  };
  sendToServer(msg);
  document.getElementById("text").value = "";
}

// 键盘事件处理程序。这用于拦截返回和输入键，以便我们可以调用send()将输入的文本传输到服务器。
function handleKey(evt) {
  if (evt.keyCode === 13 || evt.keyCode === 14) {
    if (!document.getElementById("send").disabled) {
      handleSendButton();
    }
  }
}

/*
 ====================================其它============================================
 ====================================================================================
*/
// 处理错误报告。目前，我们只是将内容转储到控制台，但在实际应用程序中，应该显示适当的(用户友好的)错误消息。
function reportError(errMessage) {
  log_error("Error " + errMessage.name + ": " + errMessage.message);
}
// 打印日志
function log(text) {
  var time = new Date();
  console.log("[" + time.toLocaleTimeString() + "] " + text);
}
// 打印错误日志
function log_error(text) {
  var time = new Date();
  console.error("[" + time.toLocaleTimeString() + "] " + text);
}
